package weeny.pre.face;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.border.TitledBorder;
import javax.swing.table.AbstractTableModel;

import prefuse.data.CascadedTable;
import prefuse.data.Table;
import prefuse.data.Tuple;
import prefuse.data.column.Column;
import prefuse.data.event.EventConstants;
import prefuse.data.event.TableListener;
import prefuse.data.expression.Predicate;
import prefuse.data.parser.DataParseException;
import prefuse.data.parser.ObjectParser;
import prefuse.data.util.ColumnProjection;
import prefuse.data.util.NamedColumnProjection;
import prefuse.util.collections.IntIterator;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;
import weeny.pre.io.parser.ColorParser;
import weeny.pre.io.parser.EnumParser;
import weeny.util.logs.Logs;

import static weeny.pre.face.ColorTextField.COLOR_FLD_PATTERN;
import static weeny.pre.io.parser.EnumParser.SHAPE_PARSER;

/**
 * This class gives an interface to the actual table in prefuse, It requires a
 * source table, a row filter, and a column filter.
 * 
 * 1) Everything changes on the data side will be shown in the table.
 * 2) Everything edited in the table will affect the tuples behind it.
 * 3) Sort is enabled by clicking the table header, of course only when that
 *    column is sotable.
 * 4) Column filter can be adjusted by right clicking on the table header and
 *    selecting on the fly.
 * 5) If the column name contains "Color", such as "_fillColor", it is rendered
 *    as a color box, double click it, you will see the actual RGBA values, and
 *    right click on the opened cell will bring up a color chooser for you to
 *    pick up a color. More details can be found in class membrs: colorEditor
 *    and colorRenderer.
 * 6) The boolean columns are rendered as checkboxes 
 * 7) There are String<-->Object parsers between the prefuse table and the java
 *    table, the parsers are placed in {@link prefuse.data.column.Column}.
 *    There are quite a few predefined parsers to help. But in case some object
 *    has no parser, it uses ObjectParser by default, which only use toString
 *    function to display it. In that case, it cannot help you parse the string
 *    values to the backend data objects. But you can add a suitable parser
 *    manually to the Column object. 
 * 8) We also show some cells as comboboxes, This behavior is decided by the 
 *    the parser of the column being EnumParser. If the parser is EnumParser,
 *    we can call getKeys() from the parser to generate a combobox, the pasering
 *    and formating tasks are also deligated to the {@link EnumParser}.
 * 9) You can call {@link #setFilter(FilterBox)} to assign a filter box to
 *    intactively filter the items in the table. For more details, please see
 *    {@link FilterBox}
 * 10)There is a wrap() function to create a JScrollpane to hold the displayed
 *    table. It will listen to the row count, when there is no row to display, 
 *    the whole jscroll panel will automatically disappear to save space.
 * 11)There is no eleven~, at least for now~~
 * 
 * @author weeny
 *
 */
public class PreJTable extends JTable{
	
	public static Logger LOG = Logs.log(PreJTable.class);
	
	protected Table srcTable = null;
	protected CascadedTable subTable = null;
	protected JPopupMenu colFilterPopup = new JPopupMenu();
	protected JScrollPane scrollPane = null;
	public JScrollPane scrollPane(){
		return scrollPane;
	}
	protected ActionListener columnFilterListener = new ActionListener() {
		//listen to columnFilterPopup, change displayed columns if necessary
		public void actionPerformed(ActionEvent e) {
			List<String> selectedList = new ArrayList<String>();
			for(int i =0 ; i<colFilterPopup.getComponentCount(); i++){
				JCheckBox box = (JCheckBox) colFilterPopup.getComponent(i);
				if (box.isSelected()) {
					selectedList.add(box.getText());
				}
			}
			String[] names = selectedList.toArray(new String[0]);
			ColumnProjection proj = new NamedColumnProjection(names, true);
			subTable.setColumnProjection(proj);
			preTableModel = new PreTableModel();
			setModel(preTableModel);
			resetCellRendererEditor();
			updateUI();
		}
	};

	public void setFilter(Predicate filter){
		subTable.setRowFilter(filter);
	}
	public void setFilter(FilterBox filter){
		filter.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (! (e.getSource() instanceof FilterBox)) {
					return;
				}
				Predicate filter = ((FilterBox)e.getSource()).getPred();
				subTable.setRowFilter(filter);
			}
		});
	}

	public class PreTableModel extends AbstractTableModel{
		protected TableListener srcUpdateListener = new TableListener() {
			public void tableChanged(Table t, int s, int e, int c, int type) {
				if ( type == EventConstants.INSERT){
		            m_rowmap = null; // invalidate row map
				}
				if ( type == EventConstants.DELETE){
					m_rowmap = null; // invalidate row map
				}
				fireTableCellUpdated(s, c);
				PreJTable.this.revalidate();
				PreJTable.this.repaint();
			}
		};
		protected int[] m_rowmap = null;
		protected String sortingField = null;
		protected boolean ascend = true;


		public PreTableModel() {
			subTable.addTableListener(srcUpdateListener);
		}
		public void headClicked(String name, MouseEvent e){
			if (e.getButton() == 3) {
				for (int i = 0; i<colFilterPopup.getComponentCount(); i++){
					JCheckBox box = (JCheckBox) colFilterPopup.getComponent(i);
					box.removeActionListener(columnFilterListener);
					boolean flag = subTable.getColumn(box.getText()) != null; 
					box.setSelected(flag);
					box.addActionListener(columnFilterListener);
				}
				colFilterPopup.show(PreJTable.this, e.getX(), e.getY());
				return;
			}
			Class columnType = subTable.getColumnType(name);
			if (!Comparable.class.isAssignableFrom(columnType)){
				if (! columnType.isPrimitive()){
					LOG.warning("DO NOTHING, ["+name+"] is not sortable");
					return;
				}
			}
			if (name.equals(sortingField)) {
				ascend = !ascend;
			}
			else{
				sortingField = name;
			}
			LOG.info("Sorting by ["+name+"] ("+(ascend?"Ascend":"Descend")+")");
			m_rowmap = null;
		}
		@Override
		public String getColumnName(int column) {
			return subTable.getColumnName(column);
		}
		protected void initRowMap() {
			m_rowmap = new int[subTable.getRowCount()];
			IntIterator rows = subTable.rows();
			if (sortingField != null) {
				rows = subTable.rowsSortedBy(sortingField, true);
			}
			for ( int i=0; rows.hasNext(); ++i ) {
				m_rowmap[ascend? i : m_rowmap.length-1-i] = rows.nextInt();
			}
		}
		protected int dataRow(int rowIndex) {
			if ( m_rowmap == null )
				initRowMap();
			return m_rowmap[rowIndex];
		}
		public int getColumnCount() {
			return subTable.getColumnCount();
		}

		public int getRowCount() {
			return subTable.getRowCount();
		}

		public Object getValueAt(int rowIndex, int columnIndex) {
			int dataRow = dataRow(rowIndex);
			String field = subTable.getColumnName(columnIndex);
			Object data = subTable.get(dataRow, columnIndex);
			Tuple tuple = subTable.getTuple(dataRow);
			return toView(field, data, tuple);
		}
		@Override
		public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
			int dataRow = dataRow(rowIndex);
			String field = subTable.getColumnName(columnIndex);
			Tuple tuple = subTable.getTuple(dataRow);
			try{
				Object value = toData(field, aValue, tuple);
				if (subTable.get(dataRow, columnIndex).equals(value)){
					LOG.info("Ignore setting ["+field+"] (SAME): "+value);
				}
				else {
					subTable.set(dataRow, columnIndex, value);
					LOG.info("Setting ["+field+"] with value = "+value);
				}
			}
			catch (Exception e) {
				String info = "cannot set the value "+aValue+" to "+field;
				LOG.severe("Use default ("+info+")");
				Object defaultValue = subTable.getDefault(field);
				subTable.set(dataRow, columnIndex, defaultValue);
			}
			
			//try to find a visual table and repaint the display visualization
			Table tempTable = subTable;
			while (tempTable instanceof CascadedTable){
				tempTable = ((CascadedTable)tempTable).getParentTable();
				if (tempTable instanceof VisualTable) {
					((VisualTable) tempTable).getVisualization().repaint();
					break;
				}
			}
		}
		@Override
		public boolean isCellEditable(int rowIndex, int columnIndex) {
			//we cannot edit the column which does not know how to 
			//parse string values
			String field = subTable.getColumnName(columnIndex);
			Column column = subTable.getColumn(field);
			return !(column.getParser() instanceof ObjectParser);
		}
		@Override
		public Class<?> getColumnClass(int columnIndex) {
			if (subTable.getColumn(columnIndex).canGetBoolean()) {
				return Boolean.class;
			}
			return super.getColumnClass(columnIndex);
		}

		protected Object toView(String field, Object data, Tuple tuple){
			Column column = subTable.getColumn(field);
			if (column.canGetBoolean()) {
				return data;
			}
			else if (!(column.getParser() instanceof ObjectParser)){
				return column.getParser().format(data);
			}
			else{
				return data;
			}
		}
		protected Object toData(String field, Object view, Tuple tuple){
			Column column = subTable.getColumn(field);
			if (column.canGetBoolean()){
				return view;
			}
			else if (!(column.getParser() instanceof ObjectParser)){
				try {
					return column.getParser().parse(view.toString());
				} catch (DataParseException e) {
					LOG.severe("cannot parse: "+view+", use previous value");
					return tuple.get(field);
				}
			}
			else{
				return view;
			}
		}
	}
	protected PreTableModel preTableModel = null;
	
	public PreJTable(Table table) {
		this(table, null, null);
	}
	public PreJTable(Table table, Predicate rowFilter){
		this(table, rowFilter, null);
	}
	public PreJTable(Table table, ColumnProjection colFilter){
		this(table, null, colFilter);
	}

	private ColorTextField colorEditorRenderer = new ColorTextField();
	private EnumComboBox shapeEditor = new EnumComboBox(SHAPE_PARSER);
	
	private void resetCellRendererEditor(){
		colorEditorRenderer.setBorder(null);
		ColorParser colorParser = new ColorParser();
		for(int i = 0; i<getModel().getColumnCount(); i++){
			String fld = getModel().getColumnName(i);
			Column col = subTable.getColumn(fld);
			boolean isColorFld = COLOR_FLD_PATTERN.matcher(fld).matches();
			//seting the renderers/editors
			if (isColorFld && subTable.canGetInt(fld)) {
				col.setParser(colorParser);
				getColumn(fld).setCellEditor(colorEditorRenderer);
				getColumn(fld).setCellRenderer(colorEditorRenderer);
			}
			else if (fld.equals(VisualItem.SHAPE)){
				col.setParser(SHAPE_PARSER);
				getColumn(fld).setCellEditor(shapeEditor);
			}
			else {
				getColumn(fld).setCellEditor(null);
				getColumn(fld).setCellRenderer(null);
			}
		}
	}
	protected void resetColumnFilterPopup(){
		colFilterPopup.removeAll();
		//init the column selector popup menu
		List<String> list = new ArrayList<String>();
		for(int i = 0; i<srcTable.getColumnCount(); i++){
			list.add(srcTable.getColumnName(i));
		}
		Collections.sort(list);
		ColumnProjection cFilter = subTable.getColumnProjection();
		for (String column : list) {
			boolean on = cFilter.include(srcTable.getColumn(column), column);
			JCheckBox check = new JCheckBox(column, on);
			check.addActionListener(columnFilterListener);
			colFilterPopup.add(check);
		}
	}
	public PreJTable(Table table, Predicate rFilter, ColumnProjection cFilter){
		this.srcTable = table;
		this.subTable = new CascadedTable(table, rFilter, cFilter);
		
		resetColumnFilterPopup();

		this.preTableModel = new PreTableModel();
		setModel(preTableModel);
		getTableHeader().addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				int idx = getColumnModel().getColumnIndexAtX(e.getX());
				preTableModel.headClicked(getColumnName(idx), e);
				updateUI();
			}
		});
		this.srcTable.addTableListener(new TableListener() {
			public void tableChanged(Table t, int s, int e, int c, int type) {
				if (type == EventConstants.UPDATE) {
					//updating action, exit
					return;
				}
				if (c == EventConstants.ALL_COLUMNS) {
					//row action, such as adding and deleting, exit
					return;
				}
				resetColumnFilterPopup();
			}
		});
		resetCellRendererEditor();
	}
	
	private int height(){
		//TODO hard code the height, do not know why -_-||
		//writing 15 is because I hard code to set the header height to be 15
		return ((subTable.getRowCount()+2)*getRowHeight()+15);
	}
	
	public static final Color LABEL_GRAY = new JLabel().getBackground();
	
	public JScrollPane scrollView(final String title, final Dimension maxDim){
		if (scrollPane != null){
			return scrollPane;
		}
		JLabel label = (JLabel) getTableHeader().getDefaultRenderer();
		//set the header height, we hard code it using 15 pixels.
		label.setPreferredSize(new Dimension(0,15));
		scrollPane = new JScrollPane(this);
		final TitledBorder border = new TitledBorder(title){
			public void paintBorder(Component c, Graphics g, int x, int y,
					int width, int height) {
				Color color = g.getColor();
				g.setColor(LABEL_GRAY);
				g.fillRect(x, y, width, height);
				super.paintBorder(c, g, x, y, width, height);
				g.setColor(color);
			}
		};
		border.setTitle(title+" ["+subTable.getRowCount()+"]");		
		scrollPane.setBorder(border);

		//for the first time, set the pane size
		if (height()<maxDim.height){
			Dimension dim = new Dimension(maxDim.width, height());
			scrollPane.setPreferredSize(dim);
		}
		else{
			scrollPane.setPreferredSize(maxDim);
		}
		//if the item number changes, update the pane size if necessary, i.e.
		//if the number is too small, shrink the size, otherwise use maxDim
		subTable.addTableListener(new TableListener() {
			public void tableChanged(Table t, int s, int e, int c, int type) {				
				int actualRowCount = t.getRowCount();
				if (type == EventConstants.DELETE) {
					//because this func is called before actual deletion,
					//the getRowCount is not the actual size after deletion
					if (c == EventConstants.ALL_COLUMNS){
						//means it is a row deletion, otherwise it is a column
						//deletion and we do not need to change the rowCount
						actualRowCount -= (e-s+1);
					}
				}
				border.setTitle(title+" ["+actualRowCount+"]");				
				if (type != EventConstants.INSERT) {
					if (type != EventConstants.DELETE) {						
						return;
					}
				}
				if (c != EventConstants.ALL_COLUMNS) {
					columnFilterListener.actionPerformed(null);
				}
				if (height() < maxDim.height){					
					Dimension d = new Dimension(maxDim.width, height());					
					scrollPane.setPreferredSize(d);
					((JComponent)scrollPane.getParent()).updateUI();
				}
				else{
					scrollPane.updateUI();
				}
				if (actualRowCount == 0) {
					scrollPane.setVisible(false);
				}
				else{
					scrollPane.setVisible(true);
				}
			}
		});
		scrollPane.setVisible(subTable.getTupleCount() != 0);
		return scrollPane;
	}
}